page.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { UNA_URL, UNA_API_KEY, MULTITENANT, PREVIEW_DOMAIN, appSetting } from 'app/config';
  2. import { cache } from 'react'
  3. import Root from 'app/root-client'
  4. import { Suspense } from 'react'
  5. import { notFound } from 'next/navigation'
  6. import { headers } from "next/headers"
  7. import { resolveLangCode } from '../../lib/resolve-lang'
  8. import { cachedUnaPageJson, unaPageCacheKey } from '../../lib/una-page-cache'
  9. const SITE_TITLE = appSetting('config', 'title');
  10. async function loadPage(props) {
  11. const [params, searchParams] = await Promise.all([
  12. props.params,
  13. props.searchParams,
  14. ]);
  15. return getData(params.path.join('/'), JSON.stringify(searchParams));
  16. }
  17. // Primitive arguments let metadata and page rendering share this request's fetch.
  18. const getData = cache(async (path, searchParamsKey) => {
  19. const search_params = JSON.parse(searchParamsKey);
  20. // Early check for static files — before any logs or UNA requests
  21. const staticFileExtensions = ['.map', '.js', '.css', '.json', '.png', '.jpg', '.svg', '.ico', '.woff', '.woff2', '.ttf'];
  22. const isStaticFile = staticFileExtensions.some(ext => path.endsWith(ext));
  23. if (isStaticFile || path.startsWith('_next/') || path.startsWith('static/')) {
  24. // Return 404 without logging or UNA requests
  25. return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: 404 };
  26. }
  27. const hdrs = await headers();
  28. // The viewer's Cookie header exactly as the browser sent it — forwarded to UNA.
  29. const cookieString = hdrs.get('cookie') || '';
  30. // Tenant headers come from proxy.js only (it drops client-sent ones); trust them
  31. // only when the proxy can set them: multi-tenant mode or CI preview hosts.
  32. const trustTenantHeaders = MULTITENANT || !!PREVIEW_DOMAIN;
  33. const tenantUnaUrl = trustTenantHeaders ? hdrs.get('x-tenant-una-url') : null;
  34. const tenantUnaKey = trustTenantHeaders ? hdrs.get('x-tenant-una-key') : null;
  35. const fetchHeaders = {
  36. cookie: cookieString,
  37. authorization: 'Bearer ' + (tenantUnaKey ?? UNA_API_KEY),
  38. // The page is rendered for a NEO web viewer: UNA records site actions with this platform.
  39. 'x-neo-platform': 'web',
  40. };
  41. let l = tenantUnaUrl ?? UNA_URL;
  42. l += '/api.php' + '?r=system/get_page_by_request/TemplServicePages&params[]=' + path;
  43. const langCode = resolveLangCode(cookieString, hdrs?.get('accept-language') || '');
  44. if (langCode) {
  45. l += '&lang=' + encodeURIComponent(langCode);
  46. }
  47. let searchParams = { ...search_params };
  48. delete searchParams.cookieString; // legacy: old proxy put cookies in the URL
  49. delete searchParams.path;
  50. delete searchParams._rsc;
  51. let sBlocks = '';
  52. if (searchParams.blocks) {
  53. sBlocks = searchParams.blocks;
  54. }
  55. delete searchParams.blocks;
  56. if (sBlocks != '' && Object.keys(searchParams).length == 0) {
  57. l = l + '&params[]=' + sBlocks;
  58. }
  59. if (Object.keys(searchParams).length > 0) {
  60. l = l + '&params[]=' + sBlocks + '&params[]=' + JSON.stringify(searchParams);
  61. }
  62. const loadUnaPage = async () => {
  63. let res;
  64. try {
  65. res = await fetch(l, {
  66. headers: fetchHeaders,
  67. cache: 'no-store',
  68. })
  69. } catch (error) {
  70. console.error('Server fetch failed (network/connection):', error);
  71. return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: 503 };
  72. }
  73. /* if (!res.ok) {
  74. const body = await res.text().catch(() => '');
  75. console.error('Server fetch failed (http):', res.status, body);
  76. return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: res.status };
  77. }*/
  78. // Read the body once: a clone kept for the error branch buffered every page twice.
  79. let text = '';
  80. try {
  81. text = await res.text();
  82. return JSON.parse(text);
  83. } catch (error) {
  84. const fatal = text.match(/Fatal error[^<]*/i)?.[0]
  85. || text.match(/Uncaught [\w\\]+:[^\n<]+/)?.[0]
  86. || text.slice(0, 200);
  87. console.error('UNA page JSON parse failed:', fatal);
  88. return {
  89. data: { title: SITE_TITLE, description: SITE_TITLE, page_status: 503 },
  90. code: 500,
  91. };
  92. }
  93. };
  94. return cachedUnaPageJson({
  95. path,
  96. key: unaPageCacheKey({
  97. url: l,
  98. tenant: tenantUnaUrl ?? '',
  99. cookieString: cookieString || '',
  100. }),
  101. load: loadUnaPage,
  102. });
  103. });
  104. export const viewport = {
  105. width: 'device-width',
  106. initialScale: 1,
  107. //maximumScale: 1, // Prevent iOS auto-zoom on input focus
  108. viewportFit: 'cover',
  109. themeColor: [
  110. { media: '(prefers-color-scheme: light)', color: 'rgb(244,244,245)' },
  111. { media: '(prefers-color-scheme: dark)', color: 'rgb(9,9,11)' },
  112. ],
  113. }
  114. export async function generateMetadata(props) {
  115. const data = await loadPage(props);
  116. const description = data?.data?.description || SITE_TITLE;
  117. const name = data?.data?.title || SITE_TITLE;
  118. const image = data?.data?.image;
  119. const isClientProject = UNA_URL != 'https://api.neo.so';
  120. const appleAppId = appSetting('smart_app_banner', 'apple_app_id');
  121. const appleAppArgument = appSetting('smart_app_banner', 'apple_app_argument');
  122. const other = {
  123. 'mobile-web-app-capable': 'yes',
  124. };
  125. if (appleAppId) {
  126. other['apple-itunes-app'] = appleAppArgument
  127. ? `app-id=${appleAppId}, app-argument=${appleAppArgument}`
  128. : `app-id=${appleAppId}`;
  129. }
  130. return {
  131. title: name,
  132. description: description,
  133. manifest: isClientProject ? '/static/manifest.json' : '/manifest.json',
  134. icons: {
  135. icon: isClientProject ? '/static/favicon.svg' : '/favicon.svg',
  136. },
  137. other,
  138. openGraph: {
  139. title: name,
  140. description: description,
  141. ...(image && {
  142. images: [
  143. {
  144. url: image,
  145. alt: name,
  146. },
  147. ],
  148. }),
  149. },
  150. twitter: {
  151. card: 'summary_large_image',
  152. title: name,
  153. description: description,
  154. ...(image && {
  155. images: [image],
  156. }),
  157. },
  158. }
  159. }
  160. export default async function Page(props) {
  161. const params = await props.params;
  162. const path = params?.path?.join('/') || '';
  163. const isHomePage = path === '' || path === 'home' || path === 'index';
  164. const data = await loadPage(props);
  165. // Static file 404 — call notFound immediately
  166. if (data?.code === 404 && !isHomePage) {
  167. notFound();
  168. }
  169. // Handle API errors gracefully - especially for home/splash page
  170. // UNA may return errors for guest users when certain blocks (e.g., messenger)
  171. // try to access user context. The frontend should still render the splash page.
  172. const hasApiError = data?.code === 500 || data?.code === 503;
  173. if (hasApiError && isHomePage) {
  174. console.warn('UNA API error on home page - rendering with fallback data for splash screen');
  175. // Provide minimal fallback data so splash page can render
  176. const fallbackData = {
  177. title: SITE_TITLE,
  178. description: SITE_TITLE,
  179. uri: '/',
  180. url: '/',
  181. page_name: 'home',
  182. page_type: 'home',
  183. logged: 0,
  184. // Empty blocks - splash will render static content
  185. blocks: {}
  186. };
  187. return (
  188. <Suspense fallback={null}>
  189. <Root path={'home'} data={fallbackData} uri={'/'} url={'/'} code={200} />
  190. </Suspense>
  191. );
  192. }
  193. if (data?.data?.page_status == 404) {
  194. notFound(props)
  195. }
  196. return (
  197. <Suspense fallback={null}>
  198. <Root path={path} data={data?.data} uri={data?.data?.uri} url={data?.data?.url} code={data?.code}></Root>
  199. </Suspense>
  200. )
  201. }